package xyz.riceball.security.authorization;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.AntPathMatcher;
import xyz.riceball.develop.entity.po.SqlDefinitionPO;
import xyz.riceball.develop.mapper.SqlDefinitionMapper;
import xyz.riceball.framework.component.SpringContextHolder;
import xyz.riceball.framework.core.context.UserContext;
import xyz.riceball.framework.core.enums.ENBool;
import xyz.riceball.framework.core.enums.ENMethodType;
import xyz.riceball.framework.core.exception.RiceBallBizErrorException;
import xyz.riceball.framework.core.util.RedisUtils;
import xyz.riceball.framework.core.util.TokenUtils;
import xyz.riceball.framework.properties.TokenProperties;
import xyz.riceball.security.authentication.RiceBallAuthenticationToken;
import xyz.riceball.security.common.SecurityConstant;
import xyz.riceball.develop.controller.CommonController;
import xyz.riceball.usercore.entity.dto.LoginDTO;
import xyz.riceball.usercore.entity.po.RolePO;
import xyz.riceball.usercore.entity.po.RoutePO;
import xyz.riceball.usercore.entity.po.UserPO;
import xyz.riceball.usercore.service.RoleService;
import xyz.riceball.usercore.service.RouteService;
import xyz.riceball.usercore.service.UserService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * 自定义权限拦截
 * </p>
 *
 * @author xiaovcloud
 * @since 2022/4/27 22:40
 */
@Getter
@Setter
@Slf4j
public class RiceBallFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private final RoleService roleService;
    private final RouteService routeService;
    private final UserService userService;
    private final AntPathMatcher antPathMatcher;


    public RiceBallFilterInvocationSecurityMetadataSource() {
        this.roleService = SpringContextHolder.getBean(RoleService.class);
        this.routeService = SpringContextHolder.getBean(RouteService.class);
        this.userService = SpringContextHolder.getBean(UserService.class);
        this.antPathMatcher = new AntPathMatcher();
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        HttpServletRequest request = ((FilterInvocation) object).getRequest();
        HttpServletResponse response = ((FilterInvocation) object).getResponse();

        //option请求直接放行，排除白名单
        String methodType = request.getMethod();
        ENMethodType methodTypeEn = ENMethodType.valueOf(methodType);
        String requestUrl = request.getRequestURI();
        if (HttpMethod.OPTIONS.matches(methodType) || SecurityConstant.LOGIN_URL.equals(requestUrl)) {
            return null;
        }

        //查询出数据库无需登陆的地址
        RoutePO routePO = selectByNoLogin(methodTypeEn.getValue(), requestUrl);
        if (routePO != null) {
            if (ENBool.TRUE.intValue() == routePO.getStatus() && ENBool.TRUE.intValue() == routePO.getOnline()) {
                sendRedirect(request, response, routePO);
                return null;
            } else {
                throw new RiceBallBizErrorException("调用接口已下线");
            }
        }

        //校验token的合法性，作为凭证，不管是否是无需登录的url，必须要有此凭证
        //填充用户信息,返回用户的所有角色信息
        List<RolePO> roleList = fillUserInfo(request);

        //token不为空说明已经登陆，查询出数据库只需要需登陆的地址
        routePO = selectByLogin(methodTypeEn.getValue(), requestUrl);
        if (routePO != null) {
            if (ENBool.TRUE.intValue() == routePO.getStatus() && ENBool.TRUE.intValue() == routePO.getOnline()) {
                sendRedirect(request, response, routePO);
                return null;
            } else {
                throw new RiceBallBizErrorException("调用接口已下线");
            }
        }

        //获取到用户所拥有的权限
        List<String> roleCodeList = roleList.stream().map(RolePO::getCode).collect(Collectors.toList());
        routePO = selectRouteByRolesAndMethodType(methodTypeEn.getValue(), requestUrl, roleCodeList);
        if (routePO != null) {
            if (ENBool.TRUE.intValue() == routePO.getStatus() && ENBool.TRUE.intValue() == routePO.getOnline()) {
                sendRedirect(request, response, routePO);
                List<RolePO> routeRoleList = roleService.selectRoleByRouteId(routePO.getId());
                return SecurityConfig.createList(routeRoleList.stream().map(RolePO::getCode).toArray(String[]::new));
            } else {
                throw new RiceBallBizErrorException("调用接口已下线");
            }
        }
        log.error("未配置接口信息，请求路径：{}，请求方式：{},请求角色：{}", requestUrl, methodTypeEn.getName(), roleList);
        throw new AuthenticationServiceException("无权限访问接口：[" + methodType + "]" + requestUrl);
    }

    /**
     * 查询出无需登录权限的功能
     *
     * @param methodType
     * @param requestUrl
     * @return
     */
    private RoutePO selectByNoLogin(int methodType, String requestUrl) {
        RoutePO routePO = null;
        String routeNoLogin = RedisUtils.get(SecurityConstant.ROUTER_NO_LOGIN);
        List<RoutePO> routeNoLoginList;
        if (StrUtil.isEmpty(routeNoLogin)) {
            //查询出数据库无需登陆的地址
            routeNoLoginList = routeService.selectByNoLogin(methodType);
        } else {
            //直接去redis里取值
            routeNoLoginList = JSONUtil.toList(routeNoLogin, RoutePO.class);
        }
        for (RoutePO route : routeNoLoginList) {
            if (route.getPattern().equals(requestUrl)) {
                routePO = route;
                break;
            } else if (antPathMatcher.match(route.getPattern(), requestUrl)) {
                routePO = route;
            }
        }
        return routePO;
    }

    /**
     * 查询出需要登录的权限
     *
     * @param methodType
     * @param requestUrl
     * @return
     */
    private RoutePO selectByLogin(int methodType, String requestUrl) {
        RoutePO routePO = null;
        List<RoutePO> routeLoginList;
        String routeLogin = RedisUtils.get(SecurityConstant.ROUTER_LOGIN);
        if (StrUtil.isEmpty(routeLogin)) {
            //查询出数据库无需登陆的地址
            routeLoginList = routeService.selectByLogin(methodType);
        } else {
            //直接去redis取值
            routeLoginList = JSONUtil.toList(routeLogin, RoutePO.class);
        }
        for (RoutePO route : routeLoginList) {
            if (route.getPattern().equals(requestUrl)) {
                routePO = route;
                break;
            } else if (antPathMatcher.match(route.getPattern(), requestUrl)) {
                routePO = route;
            }
        }
        return routePO;
    }

    /**
     * 查询需要登录权限的子功能
     *
     * @param methodType
     * @param requestUrl
     * @param roleCodeList
     * @return
     */
    private RoutePO selectRouteByRolesAndMethodType(int methodType, String requestUrl, List<String> roleCodeList) {
        RoutePO routePO = null;
        List<RoutePO> routeAuthList;
        String routeLogin = RedisUtils.get(SecurityConstant.ROUTER_AUTH);
        if (StrUtil.isEmpty(routeLogin)) {
            //查询出数据库无需登陆的地址
            routeAuthList = routeService.selectRouteByRolesAndMethodType(roleCodeList, methodType);
            ;
        } else {
            //直接去redis取值
            routeAuthList = JSONUtil.toList(routeLogin, RoutePO.class);
        }
        for (RoutePO route : routeAuthList) {
            if (route.getPattern().equals(requestUrl)) {
                routePO = route;
                break;
            } else if (antPathMatcher.match(route.getPattern(), requestUrl)) {
                routePO = route;
            }
        }
        return routePO;
    }

    /**
     * 填充用户信息
     *
     * @param request
     * @return
     */
    private List<RolePO> fillUserInfo(HttpServletRequest request) {
        String token = request.getHeader(SecurityConstant.TOKEN);
        if (StrUtil.isEmpty(token)) {
            log.error("token为空，无权访问");
            throw new AccountExpiredException("无权访问，请登录！");
        }
        if (!TokenUtils.validateToken(token)) {
            log.error("token失效,超过设置时间，请重新登录！");
            throw new AccountExpiredException("登陆过期已被锁定，请重新登录！");
        }

        String s = RedisUtils.get(SecurityConstant.TOKEN_KEY + token);
        if (StrUtil.isEmpty(s)) {
            log.warn(SecurityConstant.TOKEN_KEY + token);
            log.error("token存放redis超时失效,超过设置时间，请重新登录！");
            throw new AccountExpiredException("会话过期已被锁定，请重新登录！");
        }
        TokenProperties tokenProperties = SpringContextHolder.getBean(TokenProperties.class);
        RedisUtils.set(SecurityConstant.TOKEN_KEY + token, token, tokenProperties.getExpireTime());

        //设置用户全局信息
        String userId = TokenUtils.parseToken(token, SecurityConstant.TOKEN_USER_ID, String.class);
        log.info("token中获取的userId：{}", userId);
        //获取到用户所拥有的角色
        List<RolePO> roleList = roleService.selectRoleByUserId(userId);

        List<GrantedAuthority> authList = new ArrayList<>();
        roleList.forEach(role -> {
            authList.add(new SimpleGrantedAuthority(role.getCode()));
        });

        UserPO user = userService.getById(userId);
        LoginDTO loginDTO = new LoginDTO();
        loginDTO.setUserId(userId);
        SecurityContextHolder.getContext().setAuthentication(new RiceBallAuthenticationToken(authList, loginDTO));
        UserContext.setUser(user);
        return roleList;
    }

    /**
     * 重定向
     *
     * @param request
     * @param response
     * @param route
     */
    private void sendRedirect(HttpServletRequest request, HttpServletResponse response, RoutePO route) {
        SqlDefinitionMapper sqlDefinitionMapper = SpringContextHolder.getBean(SqlDefinitionMapper.class);
        LambdaQueryWrapper<SqlDefinitionPO> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(SqlDefinitionPO::getRouteId, route.getId());
        wrapper.eq(SqlDefinitionPO::getStatus, ENBool.TRUE.intValue());
        SqlDefinitionPO sqlDefinitionPO = sqlDefinitionMapper.selectOne(wrapper);
        if (sqlDefinitionPO != null && !CommonController.EXECUTEQUERY.equals(request.getRequestURI())) {
            response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
            response.setHeader("Location", CommonController.EXECUTEQUERY + "?routeId=" + route.getId());
        }
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
